﻿#include "frontend.h"
#include "json_builder.h"
#include "parser.h"
#include "thirdparty/cxxopts/cxxopts.hpp"
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <vector>
#include <filesystem>

#if defined(__linux__)
#include <unistd.h>
#else
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif // WIN32

void split(const std::string &src, const std::string &delim,
           std::vector<std::string> &result) {
  result.clear();
  if (src.empty() || delim.empty()) {
    return;
  }
  if (delim.size() >= src.size()) {
    return;
  }
  size_t end = 0;
  size_t begin = 0;
  std::string substr;
  while (end != std::string::npos) {
    end = src.find(delim, begin);
    if (end != std::string::npos) {
      substr = src.substr(begin, end - begin);
    } else {
      substr = src.substr(begin, std::string::npos);
    }
    if (!substr.empty() && (substr != delim)) {
      result.push_back(substr);
    }
    begin = end + delim.size();
  }
}

std::string trim(const std::string &src, const std::string &delim) {
  if (src.empty()) {
    return src;
  }
  std::string s(src);
  s.erase(0, s.find_first_not_of(delim));
  s.erase(s.find_last_not_of(delim) + 1);
  return s;
}

inline std::string getExePath() {
  const static int SIZE = 512;
  char path[SIZE] = {0};
#if defined(__linux__)
  int result = readlink("/proc/self/exe", path, sizeof(path));
  if (result < 0 || (result >= SIZE - 1)) {
    return "";
  }
  path[result] = '\0';
  for (int i = result; i >= 0; i--) {
    if (path[i] == '/') {
      path[i] = '\0';
      break;
    }
  }
  return path;
#else
  ::GetModuleFileName(NULL, path, sizeof(path));
#endif // WIN32
  std::string temp(path);
  size_t index = temp.rfind("\\");
  std::string execPath;
  execPath.append(temp, 0, index);
  return execPath;
}

int Frontend::startup(int argc, const char **argv) {
  cxxopts::Options option("frontend");
  option.add_options()("f,file", "IDL file", cxxopts::value<std::string>())(
      "o,outdir", "output directory", cxxopts::value<std::string>())(
      "t,type", "output data type, [cpp, protobuf, csharp, go]",
      cxxopts::value<std::string>())("c,check", "only check IDL file")(
      "u,uuid-db", "UUID database file, default is uuid.db",
      cxxopts::value<std::string>())(
      "depend-db", "Dependency database file, default is depend.db",
      cxxopts::value<std::string>())("i,includepath", "set idl include path",
                                     cxxopts::value<std::string>())("h,help",
                                                                    "help");
  auto result = option.parse(argc, argv);
  if (!result["file"].count()) {
    std::cerr << "Forgot a file name" << std::endl;
    std::cerr << option.help();
    return 1;
  } else {
    file_ = result["file"].as<std::string>();
  }
  if (!result["outdir"].count()) {
    outputDir_ = ".";
  } else {
    outputDir_ = result["outdir"].as<std::string>();
  }
  type_ = OutputType::ALL;
  if (result["type"].count()) {
    auto str = result["type"].as<std::string>();
    if (str == "cpp") {
      type_ = OutputType::CPP;
    } else if (str == "protobuf") {
      type_ = OutputType::PB;
    } else if (str == "csharp") {
      type_ = OutputType::CSHARP;
    } else if (str == "go") {
      type_ = OutputType::GOLANG;
    } else if (str == "idl") {
      type_ = OutputType::IDL;
    } else {
      std::cerr << "Unkonw option for -t|--type:" << str << std::endl;
      std::cerr << option.help();
      return 1;
    }
  }
  if (result["help"].count()) {
    std::cerr << option.help();
    return 1;
  }
  if (result["check"].count()) {
    onlyCheck_ = true;
  }
  shortFileName_ = getShortName(file_);
  uuidDbFileName_ = getExePath() + "/uuid.db";
  if (result["uuid-db"].count()) {
    uuidDbFileName_ = result["uuid-db"].as<std::string>();
  }
  dependDbFileName_ = getExePath() + "/depend.db";
  if (result["depend-db"].count()) {
    dependDbFileName_ = result["depend-db"].as<std::string>();
  }
  // set working
  Parser::workingPath_ = "./";
  std::cout << "rpc-front is working in " << Parser::workingPath_ << std::endl;

  Parser::includePathlist_.push_back(Parser::workingPath_);
  // 给working path 赋值
  if (result["includepath"].count()) {
    Parser::includePathlist_.push_back(result["includepath"].as<std::string>());
  }
  // Load UUID database
  if (!loadUUIDDatabase()) {
    std::cerr << "load uuid database error " << std::endl;
    return 1;
  }
  // Load dependency database
  if (!loadDependencyDatabase()) {
    std::cerr << "load dependency database error " << std::endl;
    return 1;
  }
  Parser parser(this);
  if (parser.analyze(file_, std::cerr, Parser::workingPath_)) {
    if (!parser.validate(std::cerr)) {
      std::cerr << "idl check error " << parser.getError() << std::endl;
      return 1;
    }
  } else {
    std::cerr << "analyze error: " << parser.getError() << std::endl;
    return 1;
  }

  try {
    if (!onlyCheck_) {
      JsonBuilder builder(this);
      //统一添加一个idlname 以备不时之需
      writeJsonFile(parser, builder);
      writeUUIDDatabase();
      writeDependencyDatabase();
    }
  } catch (std::exception &e) {
    std::cerr << e.what();
    return 1;
  }
  return 0;
}

void Frontend::writeUUIDDatabase() {
  std::fstream fs;
  fs.open(uuidDbFileName_, std::ios::out | std::ios::trunc);
  for (auto it : uuids_) {
    fs << it.first << " " << it.second << std::endl;
  }
  fs.close();
}

bool Frontend::loadDependencyDatabase() {
  // root file import file import file
  // ......
  std::fstream fs;
  fs.open(dependDbFileName_, std::ios::in | std::ios::out | std::ios::app);
  if (!fs) {
    std::cerr << "Cannot create dependency database file";
    return false;
  }
  if (dependency_.empty()) {
    std::string str;
    while (!fs.eof()) {
      if (!getline(fs, str).fail() && !str.empty()) {
        std::vector<std::string> result;
        split(str, " ", result);
        if (result.empty()) {
          continue;
        }
        for (std::size_t i = 1; i < result.size(); i++) {
          dependency_[result[0]].emplace_back(result[i]);
        }
      }
    }
  }
  return true;
}

void Frontend::writeDependencyDatabase() {
  std::fstream fs;
  fs.open(dependDbFileName_, std::ios::out | std::ios::trunc);
  for (const auto& [name, list] : dependency_) {
    fs << name << " ";
    for (const auto& file : list) {
      fs << file << " ";
    }
    fs << std::endl;
  }
}

std::string Frontend::getShortName(const std::string &fileName) {
  auto pos1 = fileName.rfind('/');
  auto pos2 = fileName.rfind('\\');
  if ((pos1 == std::string::npos) && (pos2 == std::string::npos)) {
    return fileName;
  }
  auto pos = pos1 == std::string::npos ? pos2 : pos1;
  return std::string(fileName, pos + 1);
}

bool Frontend::checkDependency(const std::string &shortName) {
  for (const auto& [_, list] : dependency_) {
    for (const auto& name : list) {
      if (name == shortName) {
        return false;
      }
    }
  }
  return true;
}

bool Frontend::loadUUIDDatabase() {
  std::fstream fs;
  fs.open(uuidDbFileName_, std::ios::in | std::ios::out | std::ios::app);
  if (!fs) {
    std::cerr << "Cannot create UUID database file";
    return false;
  }
  if (uuids_.empty()) {
    std::string str;
    while (!fs.eof()) {
      if (!getline(fs, str).fail() && !str.empty()) {
        std::vector<std::string> result;
        split(str, " ", result);
        uuids_.emplace(std::stoull(result[0]), result[1]);
      }
    }
  }
  return true;
}

std::string Frontend::concatPath(const std::string &root, const std::string &child) {
  std::string completePath = root;
  if (child.empty()) {
    return completePath;
  }
  if (root.empty()) {
    return child;
  }
  if (root.back() != '/' && child.front() != '/') {
    completePath += '/';
    completePath += child;
  } else if (root.back() != '/' && child.front() == '/') {
    completePath += child;
  } else if (root.back() == '/' && child.front() != '/') {
    completePath += child;
  } else if (root.back() == '/' && child.front() == '/') {
    completePath.append(root.begin() + 1, root.end());
  }
  return completePath;
}

std::string getFileNameWithoutExtension(const std::string &source) {
  std::string strWithoutExt(source);
  std::size_t found = source.find_last_of('/');

  if (found != std::string::npos) {
    strWithoutExt = source.substr(found + 1);
    found = std::string::npos;
  }

  // deal file name
  found = strWithoutExt.find_first_of('.');
  if (found != std::string::npos) {
    strWithoutExt = strWithoutExt.substr(0, found);
  }

  return strWithoutExt;
}

std::string get_file_name(const std::string& fullname) {
  namespace fs = std::filesystem;
  return fs::path(fullname).filename().string();
}

void Frontend::writeJsonFile(Parser &parser, JsonBuilder &builder) {
  std::string fileName = concatPath(outputDir_, get_file_name(file_));
  // 处理文件只留下了 文件名字
  std::string idlName = getFileNameWithoutExtension(fileName);

  if (type_ == OutputType::CPP) {
    fileName += ".cpp.json";
  } else if (type_ == OutputType::PB) {
    fileName += ".protobuf.json";
  } else if (type_ == OutputType::CSHARP) {
    fileName += ".csharp.json";
  } else if (type_ == OutputType::GOLANG) {
    fileName += ".go.json";
  } else if (type_ == OutputType::IDL) {
    fileName += ".json";
  } else {
    fileName += ".cpp.json";
  }
  std::fstream fs;
  std::cout << "prepare writing file to " << fileName << std::endl;

  fs.open(fileName, std::ios::trunc | std::ios::out);
  if (!fs) {
    std::cerr << "Cannot open " << fileName << " for writing";
    return;
  }
  switch (type_) {
  case OutputType::CPP:
    builder.build(idlName, parser, fs, CppTypeOptions);
    break;
  case OutputType::PB:
    builder.build(idlName, parser, fs, ProtobufTypeOptions);
    break;
  case OutputType::CSHARP:
    builder.build(idlName, parser, fs, CSharpTypeOptions);
    break;
  case OutputType::GOLANG:
    builder.build(idlName, parser, fs, GolangTypeOptions);
    break;
  case OutputType::IDL:
    builder.build(idlName, parser, fs, IdlTypeOptions);
    break;
  case OutputType::ALL:
    builder.build(idlName, parser, fs, CppTypeOptions);
    type_ = OutputType::PB;
    writeJsonFile(parser, builder);
    break;
  default:
    break;
  }
  fs.close();

  std::cout << "Finished " << fileName << std::endl;
}

inline std::uint32_t hashCode32(const std::string &s) {
  std::uint32_t seed = 131;
  std::uint32_t hash = 0;
  for (auto &c : s) {
    hash = hash * seed + c;
  }
  return (hash & 0x7FFFFFFF);
}

inline std::uint64_t UUID64(const std::string &s1, const std::string &s2) {
  std::uint64_t h1 = hashCode32(s1);
  std::uint64_t h2 = hashCode32(s2);
  return ((h1 << 32) + h2);
}

std::string Frontend::makeServiceUUID(const std::string &fileName,
                                      const std::string &serviceName) {
  auto uuid = UUID64(fileName, serviceName);
  auto it = uuids_.find(uuid);
  if (it == uuids_.end()) {
    uuids_.emplace(uuid, fileName + "::" + serviceName);
    return std::to_string(uuid);
  } else {
    if (it->second == fileName + "::" + serviceName) {
      return std::to_string(uuid);
    } else {
      while (true) {
        uuid += 1;
        if (uuids_.find(uuid) == uuids_.end()) {
          uuids_.emplace(uuid, fileName + "::" + serviceName);
          return std::to_string(uuid);
        }
      }
    }
  }
}

bool Frontend::updateDependency(const std::string &importedFileName) {
  auto shortName = getShortName(importedFileName);
  for (const auto &name : dependency_[shortFileName_]) {
    if (name == shortName) {
      return true;
    }
  }
  if (!checkDependency(shortName)) {
    return true;
  }
  dependency_[shortFileName_].emplace_back(shortName);
  return true;
}
